//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Data.Common;

namespace Microsoft.Research.DataLayer
{
    /// <summary>
    /// BasicObject class
    /// 
    /// Provides the base for objects stored in the backend:
    /// <ul>
    /// <li>Load (single and multiple)</li>
    /// <li>Save</li>
    /// <li>Delete</li>
    /// </ul>
    /// </summary>
    /// <author>Nelson Araujo</author>
    public abstract class BasicObject : IObject
    {
        /// <summary>
        /// Object ID.
        /// </summary>
        protected Guid ObjID;

        /// <summary>
        /// Flags if the object is brand-new or already exists in the backend
        /// </summary>
        protected bool isNew = false;

        /// <summary>
        /// Valid connection to the data backend
        /// </summary>
        protected Connection Conn;
        private Connection.Storage objType;

        /// <summary>
        /// Information about creation of the object
        /// </summary>
        public EntryChange Created;

        /// <summary>
        /// Information about the last update of the object
        /// </summary>
        public EntryChange Updated;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="objType">Type of object (from Connection.Storage)</param>
        /// <param name="conn">Valid connection to the storage backend</param>
        public BasicObject(Connection.Storage objType, Connection conn)
        {
            this.objType = objType;
            this.Conn = conn;
        }

        /// <summary>
        /// Reserved to be overriden and added child specific code
        /// </summary>
        protected virtual void OnConstructor()
        {
            /* No action. Used to initialize instances with specific code */
        }

        /// <summary>
        /// When called it saves the object to the data backend. If the object is brand-new,
        /// it creates the object.
        /// </summary>
        public abstract void Save();

        /// <summary>
        /// When called it loads a fresh version of the object from the data backend.
        /// </summary>
        public abstract void Refresh();

        /// <summary>
        /// Loads objects from the data backend
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="action">Method of load being requested (All? RootOnly? Search?)</param>
        /// <param name="key">Key being searched (related to the method being used)</param>
        /// <param name="creator">Creation function to build the object from data</param>
        /// <param name="c">Valid connection to the storage backend</param>
        /// <returns>All object that matches criteria</returns>
        /// <seealso>Connection.LoadMethod</seealso>
        protected static List<T> LoadAll<T>(Connection.Action action, object key,
            Connection.ObjectCreator creator, Connection c)
        {
            return LoadAll<T>(action, key, null, creator, c);
        }

        /// <summary>
        /// Loads objects from the data backend
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="action">Method of load being requested (All? RootOnly? Search?)</param>
        /// <param name="key">Key being searched (related to the method being used)</param>
        /// <param name="creator">Creation function to build the object from data</param>
        /// <param name="c">Valid connection to the storage backend</param>
        /// <param name="field">Key to be used by search to find records</param>
        /// <returns>All object that matches criteria</returns>
        /// <seealso>Connection.LoadMethod</seealso>
        protected static List<T> LoadAll<T>(Connection.Action action, object key, string field,
            Connection.ObjectCreator creator, Connection c)
        {
            List<Parameter> mydata = new List<Parameter>();

            switch (action)
            {
                case Connection.Action.GetAll:
                    break;
                case Connection.Action.GetRoot:
                    break;
                case Connection.Action.GetByObject:
                    mydata.Add(new Parameter("ID", Parameter.ParamType.Guid,
                        Parameter.ParamDirection.In, ((IObject)key).ID));
                    mydata.Add(new Parameter("Type", Parameter.ParamType.String,
                        Parameter.ParamDirection.In, ((IObject)key).ObjType));
                    if (field != null)
                    {
                        mydata.Add(new Parameter("Field", Parameter.ParamType.String,
                            Parameter.ParamDirection.In, field));
                    }
                    break;
                case Connection.Action.Search:
                    mydata.Add(new Parameter("Text", Parameter.ParamType.String,
                        Parameter.ParamDirection.In, (string)key));
                    break;
                default:
                    throw new ArgumentException("Implement specific action for LoadAll");
            }

            try
            {
                return LoadAll<T>(action, Connection.TypeStoreMap[typeof(T)],
                    creator, ref mydata, c);
            }
            catch (RecordNotFoundException)
            {
                // No records in the database. Succeed and returns empty list.
                return new List<T>();
            }
        }

        /// <summary>
        /// Converts the object from IObject to type T. This is used by factories
        /// to cast the object coming from the storage to the correct type, without
        /// losing the strong types.
        /// </summary>
        /// <typeparam name="T">Type to convert from</typeparam>
        /// <param name="src">Object to be converted</param>
        /// <returns>Converted object</returns>
        public static T ConvertFromIObject<T>(IObject src)
        {
            return (T)src;
        }

        /// <summary>
        /// Loads an object from the data store by name
        /// </summary>
        /// <typeparam name="T">Type of object to load</typeparam>
        /// <param name="id">ID of the object to find</param>
        /// <param name="c">Valid connection to the data store</param>
        /// <param name="creator">Creation function to build the object from data</param>
        /// <returns>Object being sought</returns>
        protected static T Load<T>(Guid id, Connection c, Connection.ObjectCreator creator)
            where T : IObject
        {
            List<Parameter> mydata = new List<Parameter>();
            mydata.Add(new Parameter("ID", Parameter.ParamType.Guid, Parameter.ParamDirection.In, id));

            PipelineParameter<T> newObj = new PipelineParameter<T>(creator);
            mydata.Add(newObj);

            c.Object(Connection.TypeStoreMap[typeof(T)], Connection.Action.GetByID, ref mydata);

            return newObj.Value;
        }

        /// <summary>
        /// Loads all objects of a given type
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="creator">Creation function to build the object from data</param>
        /// <param name="c">Valid connection to the storage backend</param>
        /// <returns>All objects present in the data backend</returns>
        protected static List<T> LoadAll<T>(Connection.ObjectCreator creator, Connection c)
            where T : NamedObject<T>
        {
            List<Parameter> mydata = new List<Parameter>();
            return LoadAll<T>(Connection.Action.GetAll, Connection.TypeStoreMap[typeof(T)],
                creator, ref mydata, c);
        }

        /// <summary>
        /// Loads all objects from data store
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="action"></param>
        /// <param name="type"></param>
        /// <param name="creator"></param>
        /// <param name="mydata"></param>
        /// <param name="c"></param>
        /// <returns></returns>
        protected static List<T> LoadAll<T>(Connection.Action action, Connection.Storage type,
            Connection.ObjectCreator creator, ref List<Parameter> mydata,
            Connection c)
        {
            List<IObject> objList;

            if (mydata == null)
                mydata = new List<Parameter>();

            c.Object(type, action, ref mydata, creator, out objList);

            List<T> results = objList.ConvertAll<T>(new Converter<IObject, T>(ConvertFromIObject<T>));

            return results;
        }

        /// <summary>
        /// Returns all instances of object type T related to another object. This is used to 
        /// fetch objects that are part of a relationship (parent -> child), for example.
        /// </summary>
        /// <typeparam name="T">Type of object being fetched</typeparam>
        /// <param name="owner">Owner of the object being fetched</param>
        /// <param name="creator">Creation function to build the object from data</param>
        /// <param name="c">Valid connection to the storage backend</param>
        /// <returns>All the objects that match the criteria</returns>
        protected static List<T> LoadAll<T>(IObject owner, Connection.ObjectCreator creator, Connection c)
        {
            return LoadAll<T>(owner, null, creator, c);
        }

        /// <summary>
        /// Returns all instances of object type T related to another object. This is used to 
        /// fetch objects that are part of a relationship (parent -> child), for example.
        /// </summary>
        /// <typeparam name="T">Type of object being fetched</typeparam>
        /// <param name="owner">Owner of the object being fetched</param>
        /// <param name="creator">Creation function to build the object from data</param>
        /// <param name="c">Valid connection to the storage backend</param>
        /// <param name="field">Key to be used in the search process</param>
        /// <returns>All the objects that match the criteria</returns>
        protected static List<T> LoadAll<T>(IObject owner, string field, Connection.ObjectCreator creator, Connection c)
        {
            List<IObject> fromDB;
            c.Relation(Connection.Action.GetByObject, owner, field, Connection.TypeStoreMap[typeof(T)],
                creator, out fromDB);
            List<T> results = fromDB.ConvertAll<T>(new Converter<IObject, T>(ConvertFromIObject<T>));
            return results;
        }

        /// <summary>
        /// Provides basic save functionality. It understands the object ID
        /// </summary>
        /// <param name="mydata">Data items that compose the object</param>
        /// <remarks>If overridden, new function should return:
        /// CreatedBy/CreatedDate if creating new object or UpdatedBy+UpdatedDate if updating</remarks>
        /// <remarks>If overridden, it should return an "ID" (out/guid) parameter when creating
        /// a new item</remarks>
        protected virtual void Save(List<Parameter> mydata)
        {
            Parameter idParam = DBParamID;

            if (isNew)
            {
                idParam.Direction = Parameter.ParamDirection.Out;
            }
            else
            {
                idParam.Direction = Parameter.ParamDirection.In;
                idParam.Value = ObjID;
            }

            mydata.Add(idParam);

            Conn.Object(this.ObjType, isNew ? Connection.Action.Add : Connection.Action.Update, ref mydata);

            Parameter userOut = Parameter.GetParamByName(isNew ? "CreatedBy" : "UpdatedBy", mydata);
            Parameter dateOut = Parameter.GetParamByName(isNew ? "CreatedDate" : "UpdatedDate", mydata);

            if (isNew)
            {
                // Fetch the definitive keyword ID
                this.ObjID = (Guid)idParam.Value;
                this.Created = new EntryChange(userOut.Value as string, (DateTime)dateOut.Value);
                this.isNew = false;
            }

            this.Updated = new EntryChange(userOut.Value as string, (DateTime)dateOut.Value);
        }

        /// <summary>
        /// Provides basic save functionality. It understands the object ID
        /// </summary>
        /// <param name="mydata">Data items that compose the object</param>
        /// <param name="pipelineCreator">Creator for the new instance</param>
        /// <param name="target">Valid data store to save to</param>
        /// <remarks>If overridden, new function should return:
        /// CreatedBy/CreatedDate if creating new object or UpdatedBy+UpdatedDate if updating</remarks>
        /// <remarks>If overridden, it should return an "ID" (out/guid) parameter when creating
        /// a new item</remarks>
        protected virtual IObject SaveTo(List<Parameter> mydata, Connection target,
            Parameter pipelineCreator)
        {
            Parameter idParam = DBParamID;

            // We treat as never saved
            isNew = true;

            if (isNew)
            {
                idParam.Direction = Parameter.ParamDirection.Out;
            }
            else
            {
                idParam.Direction = Parameter.ParamDirection.In;
                idParam.Value = ObjID;
            }

            mydata.Add(idParam);

            target.Object(this.ObjType, isNew ? Connection.Action.Add : Connection.Action.Update, ref mydata);

            //
            // We fetch the object from the target store
            //
            Guid id = (Guid)idParam.Value;

            List<Parameter> mydataNew = new List<Parameter>();
            mydataNew.Add(new Parameter("ID", Parameter.ParamType.Guid, Parameter.ParamDirection.In, id));

            mydataNew.Add(pipelineCreator);

            target.Object(this.ObjType, Connection.Action.GetByID, ref mydataNew);

            return (IObject)pipelineCreator.Value;
        }

        /// <summary>
        /// Code to be executed on delete of a record
        /// </summary>
        public virtual void OnBeforeDelete()
        {
            /* No default action */
        }

        /// <summary>
        /// Deletes an object from the data backend
        /// </summary>
        /// <throws>ObjectInUseException</throws>
        public virtual void Delete()
        {
            Parameter idParam = DBParamID;
            idParam.Direction = Parameter.ParamDirection.In;
            idParam.Value = this.ObjID;

            List<Parameter> mydata = new List<Parameter>();
            mydata.Add(idParam);

            Conn.Object(this.ObjType, Connection.Action.Delete, ref mydata);
        }

        /// <summary>
        /// Returns a value indicating whether this instance and a specified 
        ///     Microsoft.Research.DataLayer.IObject object represent the same value.
        /// </summary>
        /// <param name="obj">
        /// A Microsoft.Research.DataLayer.IObject object to compare to this instance.
        /// </param>
        /// <returns>
        /// true if obj is equal to this instance; otherwise, false.
        /// </returns>
        public override bool Equals(object obj)
        {
            IObject iObj = obj as IObject;
            return iObj != null && iObj.ID.Equals(((IObject) this).ID);
        }

        /// <summary>
        /// Serves as a hash function for a particular type.
        /// </summary>
        /// <returns>
        /// A hash code for the current Object.
        /// </returns>
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        #region IObject Members

        /// <summary>
        /// Object ID
        /// </summary>
        Guid IObject.ID
        {
            get { return ObjID; }
        }

        /// <summary>
        /// Object Type
        /// </summary>
        public Connection.Storage ObjType
        {
            get { return objType; }
        }

        #endregion

        #region Helper functions
        /// <summary>
        /// Helper property to expose the Parameter("ID") to be used with Connection
        /// objects
        /// </summary>
        private Parameter DBParamID
        {
            get
            {
                return new Parameter("ID", Parameter.ParamType.Guid, Parameter.ParamDirection.Unknown);
            }
        }

        #endregion
    }
}
